Meistern Sie React Suspense fĂŒr den Datenabruf. Lernen Sie, LadezustĂ€nde deklarativ zu verwalten, die UX mit Transitions zu verbessern und Fehler mit Error Boundaries zu behandeln.
React Suspense Boundaries: Ein tiefer Einblick in die deklarative Verwaltung von LadezustÀnden
In der Welt der modernen Webentwicklung ist die Schaffung eines nahtlosen und reaktionsschnellen Benutzererlebnisses von gröĂter Bedeutung. Eine der hartnĂ€ckigsten Herausforderungen fĂŒr Entwickler ist die Verwaltung von LadezustĂ€nden. Vom Abrufen von Daten fĂŒr ein Benutzerprofil bis zum Laden eines neuen Abschnitts einer Anwendung sind die Momente des Wartens entscheidend. In der Vergangenheit war dies mit einem Wirrwarr aus booleschen Flags wie isLoading
, isFetching
und hasError
verbunden, die ĂŒber unsere Komponenten verstreut waren. Dieser imperative Ansatz ĂŒberlĂ€dt unseren Code, verkompliziert die Logik und ist eine hĂ€ufige Fehlerquelle, wie z. B. fĂŒr Race Conditions.
Hier kommt React Suspense ins Spiel. UrsprĂŒnglich fĂŒr Code-Splitting mit React.lazy()
eingefĂŒhrt, wurden seine FĂ€higkeiten mit React 18 drastisch erweitert, um zu einem leistungsstarken, erstklassigen Mechanismus fĂŒr die Handhabung asynchroner Operationen, insbesondere des Datenabrufs, zu werden. Suspense ermöglicht es uns, LadezustĂ€nde auf deklarative Weise zu verwalten und verĂ€ndert grundlegend, wie wir unsere Komponenten schreiben und ĂŒber sie nachdenken. Anstatt zu fragen âLade ich gerade?â, können unsere Komponenten einfach sagen: âIch benötige diese Daten zum Rendern. WĂ€hrend ich warte, zeige bitte diese Fallback-UI an.â
Dieser umfassende Leitfaden nimmt Sie mit auf eine Reise von den traditionellen Methoden der Zustandsverwaltung zum deklarativen Paradigma von React Suspense. Wir werden untersuchen, was Suspense Boundaries sind, wie sie sowohl fĂŒr Code-Splitting als auch fĂŒr den Datenabruf funktionieren und wie man komplexe Lade-UIs orchestriert, die Ihre Benutzer erfreuen, anstatt sie zu frustrieren.
Der alte Weg: Die MĂŒhsal manueller LadezustĂ€nde
Bevor wir die Eleganz von Suspense voll und ganz wĂŒrdigen können, ist es wichtig, das Problem zu verstehen, das es löst. Schauen wir uns eine typische Komponente an, die Daten mit den useEffect
- und useState
-Hooks abruft.
Stellen Sie sich eine Komponente vor, die Benutzerdaten abrufen und anzeigen muss:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Zustand fĂŒr neue userId zurĂŒcksetzen
setIsLoading(true);
setUser(null);
setError(null);
const fetchUser = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Netzwerkantwort war nicht in Ordnung');
}
const data = await response.json();
setUser(data);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};
fetchUser();
}, [userId]); // Erneut abrufen, wenn sich userId Àndert
if (isLoading) {
return <p>Profil wird geladen...</p>;
}
if (error) {
return <p>Fehler: {error.message}</p>;
}
return (
<div>
<h1>{user.name}</h1>
<p>E-Mail: {user.email}</p>
</div>
);
}
Dieses Muster ist funktional, hat aber mehrere Nachteile:
- Boilerplate-Code: Wir benötigen mindestens drei Zustandsvariablen (
data
,isLoading
,error
) fĂŒr jede einzelne asynchrone Operation. Dies skaliert in einer komplexen Anwendung schlecht. - Verstreute Logik: Die Rendering-Logik ist mit bedingten PrĂŒfungen (
if (isLoading)
,if (error)
) fragmentiert. Die primĂ€re âHappy Pathâ-Renderlogik wird ganz nach unten geschoben, was die Komponente schwerer lesbar macht. - Race Conditions: Der
useEffect
-Hook erfordert eine sorgfĂ€ltige Verwaltung der AbhĂ€ngigkeiten. Ohne ordnungsgemĂ€Ăe Bereinigung könnte eine schnelle Antwort von einer langsamen Antwort ĂŒberschrieben werden, wenn sich dieuserId
-Prop schnell Ă€ndert. Obwohl unser Beispiel einfach ist, können komplexe Szenarien leicht subtile Fehler einfĂŒhren. - Wasserfall-Abrufe: Wenn eine untergeordnete Komponente ebenfalls Daten abrufen muss, kann sie nicht einmal mit dem Rendern (und damit dem Abrufen) beginnen, bis die ĂŒbergeordnete Komponente das Laden abgeschlossen hat. Dies fĂŒhrt zu ineffizienten Datenlade-WasserfĂ€llen.
BĂŒhne frei fĂŒr React Suspense: Ein Paradigmenwechsel
Suspense stellt dieses Modell auf den Kopf. Anstatt dass die Komponente den Ladezustand intern verwaltet, kommuniziert sie ihre AbhĂ€ngigkeit von einer asynchronen Operation direkt an React. Wenn die benötigten Daten noch nicht verfĂŒgbar sind, âunterbrichtâ die Komponente das Rendern.
Wenn eine Komponente unterbricht, geht React den Komponentenbaum nach oben, um die nÀchste Suspense Boundary zu finden. Eine Suspense Boundary ist eine Komponente, die Sie in Ihrem Baum mit <Suspense>
definieren. Diese Boundary rendert dann eine Fallback-UI (wie einen Spinner oder einen Skeleton Loader), bis alle Komponenten innerhalb dieser Boundary ihre DatenabhÀngigkeiten aufgelöst haben.
Die Kernidee besteht darin, die DatenabhĂ€ngigkeit bei der Komponente zu platzieren, die sie benötigt, wĂ€hrend die Lade-UI auf einer höheren Ebene im Komponentenbaum zentralisiert wird. Dies bereinigt die Komponentenlogik und gibt Ihnen eine leistungsstarke Kontrolle ĂŒber das Ladeerlebnis des Benutzers.
Wie âunterbrichtâ eine Komponente das Rendern?
Die Magie hinter Suspense liegt in einem Muster, das auf den ersten Blick ungewöhnlich erscheinen mag: das Werfen eines Promise. Eine Suspense-fÀhige Datenquelle funktioniert so:
- Wenn eine Komponente nach Daten fragt, prĂŒft die Datenquelle, ob sie die Daten zwischengespeichert hat.
- Wenn die Daten verfĂŒgbar sind, gibt sie diese synchron zurĂŒck.
- Wenn die Daten nicht verfĂŒgbar sind (d. h. sie werden gerade abgerufen), wirft die Datenquelle das Promise, das die laufende Abfrageanforderung darstellt.
React fĂ€ngt dieses geworfene Promise ab. Es stĂŒrzt Ihre App nicht ab. Stattdessen interpretiert es dies als Signal: âDiese Komponente ist noch nicht bereit zum Rendern. Pausiere sie und suche nach einer Suspense Boundary darĂŒber, um ein Fallback anzuzeigen.â Sobald das Promise aufgelöst ist, versucht React erneut, die Komponente zu rendern, die nun ihre Daten erhĂ€lt und erfolgreich rendert.
Die <Suspense>
-Boundary: Ihr Deklarator fĂŒr die Lade-UI
Die <Suspense>
-Komponente ist das HerzstĂŒck dieses Musters. Sie ist unglaublich einfach zu verwenden und benötigt eine einzige, erforderliche Prop: fallback
.
import { Suspense } from 'react';
function App() {
return (
<div>
<h1>Meine Anwendung</h1>
<Suspense fallback={<p>Inhalt wird geladen...</p>}>
<SomeComponentThatFetchesData />
</Suspense>
</div>
);
}
In diesem Beispiel sieht der Benutzer die Meldung âInhalt wird geladen...â, wenn SomeComponentThatFetchesData
unterbricht, bis die Daten bereit sind. Das Fallback kann jeder gĂŒltige React-Knoten sein, von einem einfachen String bis zu einer komplexen Skeleton-Komponente.
Klassischer Anwendungsfall: Code Splitting mit React.lazy()
Die etablierteste Verwendung von Suspense ist das Code Splitting. Es ermöglicht Ihnen, das Laden des JavaScript fĂŒr eine Komponente aufzuschieben, bis sie tatsĂ€chlich benötigt wird.
import React, { Suspense, lazy } from 'react';
// Der Code dieser Komponente wird nicht im initialen Bundle enthalten sein.
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h2>Einige Inhalte, die sofort geladen werden</h2>
<Suspense fallback={<div>Komponente wird geladen...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
Hier wird React das JavaScript fĂŒr HeavyComponent
erst dann abrufen, wenn es zum ersten Mal versucht, es zu rendern. WÀhrend es abgerufen und geparst wird, wird das Suspense-Fallback angezeigt. Dies ist eine leistungsstarke Technik zur Verbesserung der anfÀnglichen Ladezeiten der Seite.
Die moderne Grenze: Datenabruf mit Suspense
Obwohl React den Suspense-Mechanismus bereitstellt, bietet es keinen spezifischen Client fĂŒr den Datenabruf. Um Suspense fĂŒr den Datenabruf zu verwenden, benötigen Sie eine Datenquelle, die damit integriert ist (d. h. eine, die ein Promise wirft, wenn Daten ausstehen).
Frameworks wie Relay und Next.js haben eine eingebaute, erstklassige UnterstĂŒtzung fĂŒr Suspense. Beliebte Datenabruf-Bibliotheken wie TanStack Query (ehemals React Query) und SWR bieten ebenfalls experimentelle oder volle UnterstĂŒtzung dafĂŒr.
Um das Konzept zu verstehen, erstellen wir einen sehr einfachen, konzeptionellen Wrapper um die fetch
-API, um sie Suspense-kompatibel zu machen. Hinweis: Dies ist ein vereinfachtes Beispiel zu Lehrzwecken und ist nicht produktionsreif. Es fehlt eine ordnungsgemĂ€Ăe Zwischenspeicherung und die Feinheiten der Fehlerbehandlung.
// data-fetcher.js
// Ein einfacher Cache zum Speichern von Ergebnissen
const cache = new Map();
export function fetchData(url) {
if (!cache.has(url)) {
cache.set(url, { status: 'pending', promise: fetchAndCache(url) });
}
const record = cache.get(url);
if (record.status === 'pending') {
throw record.promise; // Das ist die Magie!
}
if (record.status === 'error') {
throw record.error;
}
if (record.status === 'success') {
return record.data;
}
}
async function fetchAndCache(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Abruf fehlgeschlagen mit Status ${response.status}`);
}
const data = await response.json();
cache.set(url, { status: 'success', data });
} catch (e) {
cache.set(url, { status: 'error', error: e });
}
}
Dieser Wrapper pflegt einen einfachen Status fĂŒr jede URL. Wenn fetchData
aufgerufen wird, prĂŒft es den Status. Wenn er ausstehend ist, wirft es das Promise. Wenn er erfolgreich ist, gibt es die Daten zurĂŒck. Schreiben wir nun unsere UserProfile
-Komponente damit neu.
// UserProfile.js
import React, { Suspense } from 'react';
import { fetchData } from './data-fetcher';
// Die Komponente, die die Daten tatsÀchlich verwendet
function ProfileDetails({ userId }) {
// Versuche, die Daten zu lesen. Wenn sie nicht bereit sind, wird dies eine Unterbrechung auslösen.
const user = fetchData(`https://api.example.com/users/${userId}`);
return (
<div>
<h1>{user.name}</h1>
<p>E-Mail: {user.email}</p>
</div>
);
}
// Die Elternkomponente, die die Ladezustands-UI definiert
export function UserProfile({ userId }) {
return (
<Suspense fallback={<p>Profil wird geladen...</p>}>
<ProfileDetails userId={userId} />
</Suspense>
);
}
Sehen Sie sich den Unterschied an! Die ProfileDetails
-Komponente ist sauber und konzentriert sich ausschlieĂlich auf das Rendern der Daten. Sie hat keine isLoading
- oder error
-ZustĂ€nde. Sie fordert einfach die Daten an, die sie benötigt. Die Verantwortung fĂŒr die Anzeige eines Ladeindikators wurde auf die ĂŒbergeordnete Komponente, UserProfile
, verlagert, die deklarativ angibt, was wÀhrend des Wartens angezeigt werden soll.
Orchestrierung komplexer LadezustÀnde
Die wahre StÀrke von Suspense wird deutlich, wenn Sie komplexe UIs mit mehreren asynchronen AbhÀngigkeiten erstellen.
Verschachtelte Suspense Boundaries fĂŒr eine gestaffelte UI
Sie können Suspense Boundaries verschachteln, um ein verfeinertes Ladeerlebnis zu schaffen. Stellen Sie sich eine Dashboard-Seite mit einer Seitenleiste, einem Hauptinhaltsbereich und einer Liste der letzten AktivitÀten vor. Jede dieser Komponenten könnte ihren eigenen Datenabruf erfordern.
function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<div className="layout">
<Suspense fallback={<p>Navigation wird geladen...</p>}>
<Sidebar />
</Suspense>
<main>
<Suspense fallback={<ProfileSkeleton />}>
<MainContent />
</Suspense>
<Suspense fallback={<ActivityFeedSkeleton />}>
<ActivityFeed />
</Suspense>
</main>
</div>
</div>
);
}
Mit dieser Struktur:
- Die
Sidebar
kann erscheinen, sobald ihre Daten bereit sind, auch wenn der Hauptinhalt noch lÀdt. - Der
MainContent
und derActivityFeed
können unabhĂ€ngig voneinander laden. Der Benutzer sieht einen detaillierten Skeleton Loader fĂŒr jeden Abschnitt, was einen besseren Kontext bietet als ein einziger, seitenweiter Spinner.
Dies ermöglicht es Ihnen, dem Benutzer so schnell wie möglich nĂŒtzliche Inhalte anzuzeigen und die wahrgenommene Leistung drastisch zu verbessern.
Vermeidung von UI-"Popcorning"
Manchmal kann der gestaffelte Ansatz zu einem störenden Effekt fĂŒhren, bei dem mehrere Spinner in schneller Folge erscheinen und verschwinden, ein Effekt, der oft als âPopcorningâ bezeichnet wird. Um dies zu lösen, können Sie die Suspense Boundary weiter oben im Baum platzieren.
function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<DashboardSkeleton />}>
<div className="layout">
<Sidebar />
<main>
<MainContent />
<ActivityFeed />
</main>
</div>
</Suspense>
</div>
);
}
In dieser Version wird ein einzelnes DashboardSkeleton
angezeigt, bis alle untergeordneten Komponenten (Sidebar
, MainContent
, ActivityFeed
) ihre Daten bereit haben. Das gesamte Dashboard erscheint dann auf einmal. Die Wahl zwischen verschachtelten Boundaries und einer einzigen höherstufigen Boundary ist eine UX-Design-Entscheidung, deren Umsetzung Suspense trivial macht.
Fehlerbehandlung mit Error Boundaries
Suspense behandelt den ausstehenden Zustand eines Promise, aber was ist mit dem abgelehnten Zustand? Wenn das von einer Komponente geworfene Promise ablehnt (z. B. bei einem Netzwerkfehler), wird es wie jeder andere Rendering-Fehler in React behandelt.
Die Lösung besteht darin, Error Boundaries zu verwenden. Eine Error Boundary ist eine Klassenkomponente, die eine spezielle Lebenszyklusmethode, componentDidCatch()
oder eine statische Methode getDerivedStateFromError()
, definiert. Sie fĂ€ngt JavaScript-Fehler ĂŒberall in ihrem untergeordneten Komponentenbaum ab, protokolliert diese Fehler und zeigt eine Fallback-UI an.
Hier ist eine einfache Error Boundary-Komponente:
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Zustand aktualisieren, damit der nÀchste Render die Fallback-UI anzeigt.
return { hasError: true, error: error };
}
componentDidCatch(error, errorInfo) {
// Sie können den Fehler auch an einen Fehlerberichterstattungsdienst protokollieren
console.error("Ein Fehler wurde abgefangen:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// Sie können jede beliebige Fallback-UI rendern
return <h1>Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.</h1>;
}
return this.props.children;
}
}
Sie können dann Error Boundaries mit Suspense kombinieren, um ein robustes System zu erstellen, das alle drei ZustÀnde behandelt: ausstehend, erfolgreich und fehlerhaft.
import { Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';
import { UserProfile } from './UserProfile';
function App() {
return (
<div>
<h2>Benutzerinformationen</h2>
<ErrorBoundary>
<Suspense fallback={<p>Wird geladen...</p>}>
<UserProfile userId={123} />
</Suspense>
</ErrorBoundary>
</div>
);
}
Mit diesem Muster wird das Profil angezeigt, wenn der Datenabruf in UserProfile
erfolgreich ist. Wenn er aussteht, wird das Suspense-Fallback angezeigt. Wenn er fehlschlÀgt, wird das Fallback der Error Boundary angezeigt. Die Logik ist deklarativ, komponierbar und leicht nachvollziehbar.
Transitions: Der SchlĂŒssel zu nicht blockierenden UI-Updates
Es gibt ein letztes Puzzleteil. Betrachten Sie eine Benutzerinteraktion, die einen neuen Datenabruf auslöst, wie das Klicken auf einen âWeiterâ-Button, um ein anderes Benutzerprofil anzuzeigen. Mit dem obigen Setup wird die UserProfile
-Komponente in dem Moment, in dem der Button geklickt wird und sich die userId
-Prop Ă€ndert, erneut unterbrechen. Das bedeutet, dass das aktuell sichtbare Profil verschwindet und durch das Lade-Fallback ersetzt wird. Dies kann sich abrupt und störend anfĂŒhlen.
Hier kommen Transitions ins Spiel. Transitions sind eine neue Funktion in React 18, mit der Sie bestimmte Zustandsaktualisierungen als nicht dringend markieren können. Wenn eine Zustandsaktualisierung in eine Transition gehĂŒllt wird, zeigt React weiterhin die alte UI (den veralteten Inhalt) an, wĂ€hrend es den neuen Inhalt im Hintergrund vorbereitet. Es wird das UI-Update erst dann ĂŒbernehmen, wenn der neue Inhalt zur Anzeige bereit ist.
Die primĂ€re API dafĂŒr ist der useTransition
-Hook.
import React, { useState, useTransition, Suspense } from 'react';
import { UserProfile } from './UserProfile';
function ProfileSwitcher() {
const [userId, setUserId] = useState(1);
const [isPending, startTransition] = useTransition();
const handleNextClick = () => {
startTransition(() => {
setUserId(id => id + 1);
});
};
return (
<div>
<button onClick={handleNextClick} disabled={isPending}>
NĂ€chster Benutzer
</button>
{isPending && <span> Neues Profil wird geladen...</span>}
<ErrorBoundary>
<Suspense fallback={<p>Initiales Profil wird geladen...</p>}>
<UserProfile userId={userId} />
</Suspense>
</ErrorBoundary>
</div>
);
}
Folgendes passiert jetzt:
- Das initiale Profil fĂŒr
userId: 1
wird geladen und zeigt das Suspense-Fallback an. - Der Benutzer klickt auf âNĂ€chster Benutzerâ.
- Der
setUserId
-Aufruf ist instartTransition
gehĂŒllt. - React beginnt, die
UserProfile
-Komponente mit der neuenuserId
von 2 im Speicher zu rendern. Dies fĂŒhrt dazu, dass sie unterbricht. - Entscheidend ist, anstatt das Suspense-Fallback anzuzeigen, behĂ€lt React die alte UI (das Profil fĂŒr Benutzer 1) auf dem Bildschirm.
- Der von
useTransition
zurĂŒckgegebene boolesche WertisPending
wird zutrue
, was es uns ermöglicht, einen dezenten, inline Ladeindikator anzuzeigen, ohne den alten Inhalt zu entfernen. - Sobald die Daten fĂŒr Benutzer 2 abgerufen sind und
UserProfile
erfolgreich rendern kann, ĂŒbernimmt React das Update, und das neue Profil erscheint nahtlos.
Transitions bieten die letzte Steuerungsebene und ermöglichen es Ihnen, anspruchsvolle und benutzerfreundliche Ladeerlebnisse zu schaffen, die sich nie störend anfĂŒhlen.
Best Practices und allgemeine Ăberlegungen
- Platzieren Sie Boundaries strategisch: HĂŒllen Sie nicht jede winzige Komponente in eine Suspense Boundary. Platzieren Sie sie an logischen Punkten in Ihrer Anwendung, an denen ein Ladezustand fĂŒr den Benutzer sinnvoll ist, wie eine Seite, ein groĂes Panel oder ein wichtiges Widget.
- Entwerfen Sie aussagekrÀftige Fallbacks: Generische Spinner sind einfach, aber Skeleton Loaders, die die Form des zu ladenden Inhalts nachahmen, bieten ein viel besseres Benutzererlebnis. Sie reduzieren Layout Shift und helfen dem Benutzer zu antizipieren, welche Inhalte erscheinen werden.
- BerĂŒcksichtigen Sie die Barrierefreiheit: Stellen Sie beim Anzeigen von LadezustĂ€nden sicher, dass diese zugĂ€nglich sind. Verwenden Sie ARIA-Attribute wie
aria-busy="true"
auf dem Inhaltscontainer, um Screenreader-Benutzer darĂŒber zu informieren, dass der Inhalt aktualisiert wird. - Nutzen Sie Server Components: Suspense ist eine grundlegende Technologie fĂŒr React Server Components (RSC). Bei der Verwendung von Frameworks wie Next.js ermöglicht Suspense das Streamen von HTML vom Server, sobald Daten verfĂŒgbar werden, was zu unglaublich schnellen anfĂ€nglichen Seitenladezeiten fĂŒr ein globales Publikum fĂŒhrt.
- Nutzen Sie das Ăkosystem: Obwohl das VerstĂ€ndnis der zugrunde liegenden Prinzipien wichtig ist, sollten Sie sich fĂŒr Produktionsanwendungen auf erprobte Bibliotheken wie TanStack Query, SWR oder Relay verlassen. Sie kĂŒmmern sich um Caching, Deduplizierung und andere KomplexitĂ€ten und bieten gleichzeitig eine nahtlose Suspense-Integration.
Fazit
React Suspense reprÀsentiert mehr als nur eine neue Funktion; es ist eine grundlegende Weiterentwicklung in der Art und Weise, wie wir AsynchronitÀt in React-Anwendungen angehen. Indem wir uns von manuellen, imperativen Lade-Flags verabschieden und ein deklaratives Modell annehmen, können wir Komponenten schreiben, die sauberer, widerstandsfÀhiger und einfacher zu komponieren sind.
Durch die Kombination von <Suspense>
fĂŒr ausstehende ZustĂ€nde, Error Boundaries fĂŒr FehlerzustĂ€nde und useTransition
fĂŒr nahtlose Updates steht Ihnen ein vollstĂ€ndiges und leistungsstarkes Toolkit zur VerfĂŒgung. Sie können alles von einfachen Lade-Spinnern bis hin zu komplexen, gestaffelten Dashboard-EnthĂŒllungen mit minimalem, vorhersagbarem Code orchestrieren. Wenn Sie anfangen, Suspense in Ihre Projekte zu integrieren, werden Sie feststellen, dass es nicht nur die Leistung und das Benutzererlebnis Ihrer Anwendung verbessert, sondern auch Ihre Zustandsverwaltungslogik drastisch vereinfacht, sodass Sie sich auf das konzentrieren können, was wirklich zĂ€hlt: groĂartige Funktionen zu entwickeln.